Java Microservices - Beginner's Guide
Table of Contents
- What are Microservices?
- Monolith vs Microservices
- Key Characteristics
- Benefits and Challenges
- Java Microservices Ecosystem
- Core Concepts
- Getting Started with Spring Boot
- Communication Patterns
- Data Management
- Service Discovery
- Configuration Management
- Monitoring and Logging
- Security
- Deployment Strategies
- Best Practices
- Common Pitfalls
What are Microservices?
Microservices is an architectural approach where a large application is built as a suite of small, independent services that communicate over well-defined APIs.
Key Points:
- Each service runs in its own process
- Services are developed and deployed independently
- Services can be written in different programming languages
- Services communicate via HTTP/REST or messaging
Simple Analogy:
Think of a traditional monolithic application like a big apartment building - if you want to change the kitchen, you might affect the entire building. Microservices are like a neighborhood of houses - you can renovate one house without affecting others.
Monolith vs Microservices
Aspect | Monolith | Microservices |
---|---|---|
Architecture | Single deployable unit | Multiple independent services |
Database | Shared database | Database per service |
Technology Stack | Single technology | Mixed technologies allowed |
Deployment | Deploy entire application | Deploy services independently |
Scaling | Scale entire application | Scale individual services |
Development Team | Single team | Multiple small teams |
Complexity | Simple initially | Complex from the start |
Key Characteristics
1. Business Capability Focus
Each microservice is built around a specific business capability (e.g., User Management, Payment Processing, Inventory Management).
2. Decentralized Governance
Teams can choose their own technology stack and make independent decisions.
3. Failure Isolation
If one service fails, others continue to operate.
4. Smart Endpoints and Dumb Pipes
Services handle business logic, while communication is simple (HTTP, messaging).
5. Design for Failure
Assume services will fail and design accordingly.
Benefits and Challenges
✅ Benefits
-
Independent Development & Deployment
- Teams can work independently
- Faster release cycles
- Less coordination overhead
-
Technology Diversity
- Choose the right tool for each job
- Easier to adopt new technologies
-
Scalability
- Scale only the services that need it
- More efficient resource usage
-
Fault Tolerance
- Failure in one service doesn't bring down entire system
- Better resilience
❌ Challenges
-
Complexity
- Network calls instead of method calls
- Distributed system complexity
- More moving parts
-
Data Consistency
- No ACID transactions across services
- Eventual consistency challenges
-
Testing
- Integration testing is harder
- Need for contract testing
-
Operational Overhead
- More services to monitor
- More deployment pipelines
Java Microservices Ecosystem
Core Frameworks
- Spring Boot - Most popular Java microservices framework
- Spring Cloud - Provides microservices patterns
- Quarkus - Kubernetes-native Java stack
- Micronaut - Modern JVM-based framework
Supporting Tools
- Docker - Containerization
- Kubernetes - Container orchestration
- Maven/Gradle - Build tools
- Netflix OSS - Microservices libraries (Eureka, Hystrix)
Core Concepts
1. Service Boundaries
❌ Bad: Services sharing databases
❌ Bad: Services knowing internal details of others
✅ Good: Services with clear, well-defined interfaces
✅ Good: Services owning their data
2. Database per Service
Each microservice should have its own database to ensure loose coupling.
User Service → User DB
Order Service → Order DB
Payment Service → Payment DB
3. API Gateway
Central entry point for all client requests.
Client → API Gateway → [User Service, Order Service, Payment Service]
Getting Started with Spring Boot
1. Basic Microservice Structure
@SpringBootApplication
@RestController
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// Business logic here
return userService.findById(id);
}
}
2. Key Annotations
@SpringBootApplication
- Main application class@RestController
- REST API controller@Service
- Business logic layer@Repository
- Data access layer@Entity
- JPA entity
3. Application Properties
# application.yml
server:
port: 8081
spring:
application:
name: user-service
datasource:
url: jdbc:h2:mem:userdb
driver-class-name: org.h2.Driver
Communication Patterns
1. Synchronous Communication
REST API Calls
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public User getUserDetails(Long userId) {
String url = "http://user-service/users/" + userId;
return restTemplate.getForObject(url, User.class);
}
}
Feign Client (Declarative)
@FeignClient(name = "user-service")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id);
}
2. Asynchronous Communication
Message Queues (RabbitMQ Example)
@RabbitListener(queues = "order.created")
public void handleOrderCreated(OrderCreatedEvent event) {
// Process order created event
emailService.sendOrderConfirmation(event.getOrderId());
}
3. When to Use Each Pattern
Pattern | Use When | Example |
---|---|---|
Synchronous | Need immediate response | Get user profile |
Asynchronous | Fire-and-forget operations | Send email notification |
Data Management
1. Database per Service Pattern
✅ Good Pattern:
User Service → MySQL (Users table)
Order Service → PostgreSQL (Orders, OrderItems tables)
Inventory Service → MongoDB (Products collection)
2. Shared Database Anti-Pattern
❌ Avoid This:
User Service ↘
Shared DB
Order Service ↗
3. Data Consistency Patterns
Saga Pattern
For managing transactions across multiple services:
@Service
public class OrderSagaOrchestrator {
public void processOrder(Order order) {
try {
// Step 1: Reserve inventory
inventoryService.reserveItems(order.getItems());
// Step 2: Process payment
paymentService.processPayment(order.getPayment());
// Step 3: Create order
orderService.createOrder(order);
} catch (Exception e) {
// Compensating actions
inventoryService.releaseReservation(order.getItems());
// ... other rollback actions
}
}
}
Service Discovery
1. Netflix Eureka (Spring Cloud)
Eureka Server
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Eureka Client
@EnableEurekaClient
@SpringBootApplication
public class UserServiceApplication {
// Application code
}
2. Application Configuration
# Eureka Client Configuration
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true
Configuration Management
1. Spring Cloud Config
Config Server
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
Config Client
# bootstrap.yml
spring:
cloud:
config:
uri: http://localhost:8888
application:
name: user-service
2. Environment-Specific Configuration
config-repo/
├── user-service.yml # Default config
├── user-service-dev.yml # Development config
├── user-service-prod.yml # Production config
└── application.yml # Global config
Monitoring and Logging
1. Distributed Tracing
// Spring Cloud Sleuth automatically adds tracing
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
log.info("Getting user with id: {}", id); // Automatically traced
return userService.findById(id);
}
2. Health Checks
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
if (isDatabaseUp()) {
return Health.up().withDetail("database", "Available").build();
} else {
return Health.down().withDetail("database", "Not Available").build();
}
}
}
3. Metrics with Micrometer
@RestController
public class UserController {
private final Counter userRequestCounter;
public UserController(MeterRegistry meterRegistry) {
this.userRequestCounter = Counter.builder("user.requests")
.description("Number of user requests")
.register(meterRegistry);
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
userRequestCounter.increment();
return userService.findById(id);
}
}
Security
1. OAuth2 with JWT
@EnableWebSecurity
@EnableResourceServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
2. Service-to-Service Authentication
@Configuration
public class FeignClientConfig {
@Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return requestTemplate -> {
String token = getCurrentUserToken();
requestTemplate.header("Authorization", "Bearer " + token);
};
}
}
Deployment Strategies
1. Docker Containerization
FROM openjdk:11-jre-slim
COPY target/user-service-1.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
2. Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0
ports:
- containerPort: 8080
Best Practices
1. Start with a Monolith
- Build a monolith first
- Extract microservices when you understand the domain boundaries
2. Service Size
- Follow the "two-pizza team" rule
- If a team can't be fed with two pizzas, the service might be too big
3. API Design
// ✅ Good: Versioned APIs
@GetMapping("/v1/users/{id}")
public User getUserV1(@PathVariable Long id) { ... }
@GetMapping("/v2/users/{id}")
public UserV2 getUserV2(@PathVariable Long id) { ... }
4. Error Handling
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
5. Circuit Breaker Pattern
@Component
public class UserServiceClient {
@CircuitBreaker(name = "user-service", fallbackMethod = "fallbackUser")
public User getUser(Long id) {
return restTemplate.getForObject("/users/" + id, User.class);
}
public User fallbackUser(Long id, Exception ex) {
return new User(id, "Unknown User"); // Fallback response
}
}
Common Pitfalls
1. Distributed Monolith
❌ Problem: Services are too tightly coupled ✅ Solution: Ensure services can be developed and deployed independently
2. Chatty Interfaces
❌ Problem: Too many API calls between services ✅ Solution: Design coarser-grained APIs
3. Shared Database
❌ Problem: Multiple services accessing the same database ✅ Solution: Database per service pattern
4. Ignoring Network Latency
❌ Problem: Treating remote calls like local calls ✅ Solution: Design for network failures and latency
5. Not Monitoring Enough
❌ Problem: Lack of observability in distributed system ✅ Solution: Comprehensive monitoring, logging, and tracing
Learning Path
Phase 1: Foundations
- Learn Spring Boot basics
- Understand REST API design
- Practice with simple CRUD applications
Phase 2: Microservices Basics
- Create multiple Spring Boot services
- Implement service-to-service communication
- Set up service discovery with Eureka
Phase 3: Advanced Patterns
- Implement API Gateway
- Add configuration management
- Set up monitoring and logging
Phase 4: Production Ready
- Add security (OAuth2/JWT)
- Implement circuit breakers
- Set up containerization and orchestration
Useful Resources
Documentation
Books
- "Microservices Patterns" by Chris Richardson
- "Building Microservices" by Sam Newman
- "Spring Microservices in Action" by John Carnell
Tools to Explore
- Docker - Containerization
- Kubernetes - Container orchestration
- Postman - API testing
- Zipkin - Distributed tracing
- Prometheus - Monitoring
- ELK Stack - Logging
Summary
Microservices architecture offers many benefits but comes with increased complexity. Start small, learn the patterns, and gradually build up your understanding. Remember:
- Domain-driven design is crucial for service boundaries
- Automation is essential for managing complexity
- Monitoring is critical for distributed systems
- Team structure should align with service architecture
- Start simple and evolve your architecture over time
The key to successful microservices is not the technology, but understanding the business domain and designing services around business capabilities.